package bsh.dyn;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import bsh.CompilationState;
import bsh.CompilationUnit;
import bsh.CompiledScript;
import bsh.exception.CompilationFailedException;
import bsh.meta.JavaClassWrapper;
import bsh.meta.JavaMethodDescriptor;
import bsh.meta.MethodDescriptor;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.BytecodeHelper;

/**
 * {@link Invocable} with an {@link ArgumentsAdaptor}
 * 
 * @author takeshi
 */
public class AdaptedInvocable implements Invocable {

    private final ArgumentsAdaptor adaptor;

    private final Invocable invocable;

    /**
     * C'tor
     * 
     * @param adaptor the {@link ArgumentsAdaptor}
     * @param invocable the {@link Invocable}
     */
    public AdaptedInvocable(ArgumentsAdaptor adaptor, Invocable invocable) {
        this.adaptor = adaptor;
        this.invocable = invocable;
    }

    /**
     * @return the {@link ArgumentsAdaptor}
     */
    public ArgumentsAdaptor adaptor() {
        return this.adaptor;
    }

    /**
     * @return {@link Invocable}
     */
    public Invocable innerInvocable() {
        return this.invocable;
    }

    /**
     * {@inheritDoc}
     */
    public Object invoke(CompiledScript script, Object target, Object[] parameters) throws Throwable {
        return this.invocable.invoke(script, target, this.adaptor.adaptArguments(script, parameters));
    }

    /**
     * {@inheritDoc}
     */
    public DeclaredParameterType[] getParameterTypes() {
        return this.invocable.getParameterTypes();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isVarargs() {
        return this.invocable.isVarargs();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.adaptor == null) ? 0 : this.adaptor.hashCode());
        result = prime * result + ((this.invocable == null) ? 0 : this.invocable.hashCode());
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        AdaptedInvocable other = (AdaptedInvocable) obj;
        if (this.adaptor == null) {
            if (other.adaptor != null) {
                return false;
            }
        } else if (!this.adaptor.equals(other.adaptor)) {
            return false;
        }
        if (this.invocable == null) {
            if (other.invocable != null) {
                return false;
            }
        } else if (!this.invocable.equals(other.invocable)) {
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.invocable.toString();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isCompilable() {
        return this.adaptor.isCompilable() && (this.invocable instanceof MethodInvocable || this.invocable instanceof ConstructorInvocable);
    }

    /**
     * Compiles a constructor invocation
     * 
     * @param visitor the {@link MethodVisitor}
     * @param unit {@link CompilationUnit}
     * @param state the {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void compileConstructor(MethodVisitor visitor, CompilationUnit unit, CompilationState state) throws CompilationFailedException {
        Constructor ctor = ((ConstructorInvocable) invocable).getConstructor();
        TypeWrapper wrapper = JavaClassWrapper.wrapperFor(ctor.getDeclaringClass());
        visitor.visitTypeInsn(Opcodes.NEW, wrapper.internalName());
        visitor.visitInsn(Opcodes.DUP);
        Class[] params = ctor.getParameterTypes();

        pushArguments(visitor, unit, state, params);

        Type[] types = new Type[params.length];
        for (int i = 0; i < types.length; i++) {
            types[i] = Type.getType(params[i]);
        }
        visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, wrapper.internalName(), "<init>",
                Type.getMethodDescriptor(Type.getType(void.class), types));
        visitor.visitInsn(Opcodes.RETURN);
    }

    /**
     * Pushes arguments to a method invocation
     * 
     * @param visitor the {@link MethodVisitor}
     * @param unit the {@link CompilationUnit}
     * @param state the {@link CompilationState}
     * @param params the array of parameters
     * @throws CompilationFailedException e
     */
    public void pushArguments(MethodVisitor visitor, CompilationUnit unit, CompilationState state, Class[] params)
            throws CompilationFailedException {
        int oldSize = state.getStackSize();
        this.adaptor.generateBytecode(visitor, unit, state);
        state.assertStackSize(oldSize + params.length);
    }

    /**
     * Compiles the method invocation. Checks for the stack size and shape to verify availability
     * 
     * @param visitor {@link MethodVisitor}
     * @param unit the {@link CompilationUnit}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void compileMethodInvocation(MethodVisitor visitor, CompilationUnit unit, CompilationState state)
            throws CompilationFailedException {
        Method m = ((MethodInvocable) invocable).getMethod();
        MethodDescriptor md = new JavaMethodDescriptor(m);
        if (!Modifier.isStatic(m.getModifiers())) {
            visitor.visitVarInsn(Opcodes.ALOAD, 2);
            visitor.visitTypeInsn(Opcodes.CHECKCAST, md.declaringType().internalName());
            state.pushStack(md.declaringType());
        }
        int oldSize = state.getStackSize();
        this.adaptor.generateBytecode(visitor, unit, state);
        state.assertStackSize(oldSize + md.parameterTypes().size());
        md.writeInvoke(visitor, state);
        BytecodeHelper.fixReturnType(visitor, m);
        visitor.visitInsn(Opcodes.ARETURN);
    }

    /**
     * {@inheritDoc}
     */
    public void generateBytecode(MethodVisitor visitor, CompilationUnit unit, CompilationState state) throws CompilationFailedException {
        if (this.invocable instanceof MethodInvocable) {
            this.compileMethodInvocation(visitor, unit, state);
        } else if (this.invocable instanceof ConstructorInvocable) {
            this.compileConstructor(visitor, unit, state);
        }
    }

}
